【SwiftUI】ARKitとRealityKitを活用し、特大のアート作品を部屋に置いてみた
油絵やアクリル画などが見るのが好きなのですが、美術館に行って驚くのは作品の大きさに驚かされます。 いつかこんな大きな絵を部屋に置けたらなと思ったりしていたのですが、ARを使うことで簡単に実現できそうだなと思い試してみることにしました。
作ったもの
はじめに
以前、ARKit
とARSCNView
を使い平面検出をして物を置くというのは試したことがあったのですが、今回はARKit
とRealityKit
とARView
を使い試してみることにしました。
環境
- Xcode 13.3
- iPhone 12mini
ContentView
今回はUIViewRepresentable
の ARViewContainer
を表示させます。
import SwiftUI struct ContentView: View { var body: some View { ARViewContainer() .ignoresSafeArea() } }
ARViewContainer
ARView
をSwiftUIで使用する為に、UIViewRepresentable
を作成します。
import RealityKit import ARKit import SwiftUI struct ARViewContainer: UIViewRepresentable { func makeUIView(context: Context) -> ARView { let arView = ARView(frame: .zero, cameraMode: .ar, automaticallyConfigureSession: true) arView.addTapGesture() return arView } func updateUIView(_ uiView: ARView, context: Context) {} }
makeUIView
ARView
を生成する関数で、今回はフレームが.zero
でカメラモードを.ar
にしています。.ar
にすることでARセッションによって管理されているデバイスのカメラを使用出来ます。
今回は特に特別な構成は使用しない為、automaticallyConfigureSession
をtrue
としました。独自の構成でセッションを手動で実行する場合は、この値をfalse
にします。デフォルトではこの値はtrue
になっています。
そして、タップした時の処理を追加したいのでARView
にarView.addTapGesture()
でUITapGestureRecognizer
を追加しています。この関数の詳細は後ほど説明致します。
updateUIView
ARView
が更新された時に実行されるupdateUIView
は今回使用していないので中身は空になっています。
extension ARView
ARView
のエクステンションメソッドを作成します。
addTapGesture()
handleTap(recognizer: UITapGestureRecognizer)
getArtMaterial(name resourceName: String)
placeCanvas(at position: SIMD3)
extension ARView { func addTapGesture() { let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(recognizer:))) self.addGestureRecognizer(tapGesture) } @objc func handleTap(recognizer: UITapGestureRecognizer) { // タップしたロケーションを取得 let tapLocation = recognizer.location(in: self) // タップした位置に対応する3D空間上の平面とのレイキャスト結果を取得 let raycastResults = raycast(from: tapLocation, allowing: .estimatedPlane, alignment: .vertical) guard let firstResult = raycastResults.first else { return } // taplocationをワールド座標系に変換 let position = simd_make_float3(firstResult.worldTransform.columns.3) placeCanvas(at: position) } /// キャンバスを配置する private func placeCanvas(at position: SIMD3<Float>) { guard let artTexture = getArtMaterial(name: "matsuda") else { return } let mesh = MeshResource.generateBox(width: 2, height: 3, depth: 0.15) let canvas = ModelEntity(mesh: mesh, materials: [artTexture]) canvas.look(at: cameraTransform.translation, from: position, relativeTo: nil) let anchorEntity = AnchorEntity(world: position) anchorEntity.addChild(canvas) scene.addAnchor(anchorEntity) } /// アートマテリアルを取得する private func getArtMaterial(name resourceName: String) -> PhysicallyBasedMaterial? { guard let texture = try? TextureResource.load(named: resourceName) else { return nil } var imageMaterial = PhysicallyBasedMaterial() let baseColor = MaterialParameters.Texture(texture) imageMaterial.baseColor = PhysicallyBasedMaterial.BaseColor(tint: .white, texture: baseColor) return imageMaterial } }
addTapGesture()
ARView
にUITapGestureRecognizer
を追加します。セレクターとしてhandleTap(recognizer:)
を指定しています。
func addTapGesture() { let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(recognizer:))) self.addGestureRecognizer(tapGesture) }
handleTap(recognizer:)
実際にタップされた時に実行する処理になります。
@objc func handleTap(recognizer: UITapGestureRecognizer) { // タップしたロケーションを取得 let tapLocation = recognizer.location(in: self) // タップした位置に対応する3D空間上の平面とのレイキャスト結果を取得 let raycastResults = raycast(from: tapLocation, allowing: .estimatedPlane, alignment: .vertical) guard let firstResult = raycastResults.first else { return } // taplocationをワールド座標系に変換 let position = simd_make_float3(firstResult.worldTransform.columns.3) placeCanvas(at: position) }
タップしたロケーションを取得
まずはタップした箇所のロケーションを取得します
let tapLocation = recognizer.location(in: self)
タップした位置に対応するレイキャスト結果を取得
タップしたロケーションを介してレイキャストを実行します。レイキャスト(raycast)とは光(ray)を投げる(cast)という意味があり、投げた光の当たった結果を今回は取得しています。
@MainActor func raycast(from point: CGPoint, allowing target: ARRaycastQuery.Target, alignment: ARRaycastQuery.TargetAlignment) -> [ARRaycastResult]
引数は下記のようになっています。
from point: CGPoint
- ビューのローカル座標系のポイント
allowing target: ARRaycastQuery.Target
- 光線が終了するターゲットのタイプ、どの程度の平面を検出するか
case estimatedPlane
ARKitが推定できる非平面のサーフェスまたは平面case existingPlaneGeometry
平面が決定的なサイズと形状を持つことを必要とするcase existingPlaneInfinite
サイズや形状に関係なく、検出された平面を指定する
- 光線が終了するターゲットのタイプ、どの程度の平面を検出するか
alignment: ARRaycastQuery.TargetAlignment
- ターゲットのalignment
case any
両方case horizontal
水平case vertical
垂直
今回は、垂直面の平面をターゲットにしたいので、ターゲットを.estimatedPlane
、アライメントを.vertical
にしています。
これによりタップした位置に対応する垂直面の平面の現実世界の位置を取得することが出来ます。
レイキャスト結果をワールド座標系のポジションに変換
guard let firstResult = raycastResults.first else { return } // taplocationをワールド座標系に変換 let position = simd_make_float3(firstResult.worldTransform.columns.3) placeCanvas(at: position)
ARRaycastResult
として、カメラから最も近いものから最も遠いものへとソートされたレイキャスト結果のリストが返ってくるので、その結果を先頭を取り出します。
worldTransform.columns.3
からx
、y
、z
の移動距離が取得できるので、その結果をSIMD3
に変換しています。
キャンバスを配置する為に変換したポジションをplaceCanvas(at:)
関数に渡しています。
キャンバスを配置する
private func placeCanvas(at position: SIMD3<Float>) { /// 3Dオブジェクトのマテリアル guard let artTexture = getArtMaterial(name: "matsuda") else { return } // 3Dオブジェクトのメッシュ let mesh = MeshResource.generateBox(width: 2, height: 3, depth: 0.15) // マテリアルとメッシュから物理オブジェクトを生成 let canvas = ModelEntity(mesh: mesh, materials: [artTexture]) // オブジェクトをカメラの方向に向ける canvas.look(at: cameraTransform.translation, from: position, relativeTo: nil) // アンカーを作成 let anchorEntity = AnchorEntity(world: position) anchorEntity.addChild(canvas) scene.addAnchor(anchorEntity) }
まず、後述するgetArtMaterial(name:)
を使用してマテリアルを取得しています。
今回は、キャンバスっぽいものを表現したかったのでボックス型のメッシュを生成、サイズは幅2m、高さ3m、奥行き15cmとしました。
オブジェクトを配置した際に意図していない方向を向かないようにする為にlook(at:from:relativeTo:)
を使用し、カメラの方向に向けることにしました。
最後に引数として渡された位置にアンカーを生成して、そのアンカーの子としてキャンバス型のオブジェクトを追加しています。そのアンカーをARView
のscene
上に追加することでキャンバスをAR空間上に配置することが出来ます。
オブジェクトに画像を貼り付ける
placeCanvas
内でマテリアルを取得しているgetArtMaterial(name:)
についての説明です。
private func getArtMaterial(name resourceName: String) -> PhysicallyBasedMaterial? { guard let texture = try? TextureResource.load(named: resourceName) else { return nil } var imageMaterial = PhysicallyBasedMaterial() let baseColor = MaterialParameters.Texture(texture) imageMaterial.baseColor = PhysicallyBasedMaterial.BaseColor(tint: .white, texture: baseColor) return imageMaterial }
まずは、貼り付けたい画像をアセットフォルダに追加しておきます。
TextureResource.load(named:)
を実行することで、バンドルにある画像をTextureResource
として読み込みすることが出来ます。
あとは、そのテクスチャをマテリアルのbaseColor
として設定してあげると完成です。
おわりに
これで自分の部屋でも特大のアート作品の鑑賞を楽しむことが出来るようになりました!閲覧できるアート作品を増やしていき、楽しんでいきたいです。
今回、RealityKit
を使用してみたのですが、ARKit
とARSCNView
を使う時とはまた勝手が違っており、新たな学びが沢山ありました。まだまだRealityKit
の勉強が足りないので引き続き学んでいきたいと思います。また、LiDAR搭載のカメラがある方はもっと精度良く楽しめると思うので手に入れたい気持ちが高まりました。
モバイルアプリ開発のチームメンバー絶賛募集中!
モバイル事業部では事業会社様と一緒に、数年間にわたり長期でモバイルアプリをグロースさせています。
そんなモバイルアプリ開発のチームメンバーを絶賛募集中です!
もちろんモバイルアプリ開発以外のエンジニアも募集中です!
参考
- init(frame:cameraMode:automaticallyConfigureSession:)
- ARView.CameraMode
- automaticallyConfigureSession
- raycast(from:allowing:alignment:)
- Raycaster(Unity)
- ARRaycastを飛ばして、ねらった平面の位置を取得する
- AR kit で顔面キャスアプリを作る (その4 transformから目の回転角を求める)
- ModelEntity
- Loading a RealityKit image texture from a URL
- PhysicallyBasedMaterial.BaseColor
- RealityKit の参考書